/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.method.support;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.SimpleSessionStatus;

/**
 * Records model and view related decisions made by
 * {@link HandlerMethodArgumentResolver}s and
 * {@link HandlerMethodReturnValueHandler}s during the course of invocation of
 * a controller method.
 *
 * <p>The {@link #setRequestHandled} flag can be used to indicate the request
 * has been handled directly and view resolution is not required.
 *
 * <p>A default {@link Model} is automatically created at instantiation.
 * An alternate model instance may be provided via {@link #setRedirectModel}
 * for use in a redirect scenario. When {@link #setRedirectModelScenario} is set
 * to {@code true} signalling a redirect scenario, the {@link #getModel()}
 * returns the redirect model instead of the default model.
 *
 * @author Rossen Stoyanchev
 * @author Juergen Hoeller
 * @since 3.1
 */
public class ModelAndViewContainer {

    private boolean ignoreDefaultModelOnRedirect = false;

    @Nullable
    private Object view;

    private final ModelMap defaultModel = new BindingAwareModelMap();

    @Nullable
    private ModelMap redirectModel;

    private boolean redirectModelScenario = false;

    @Nullable
    private HttpStatus status;

    private final Set<String> noBinding = new HashSet<>(4);

    private final Set<String> bindingDisabled = new HashSet<>(4);

    private final SessionStatus sessionStatus = new SimpleSessionStatus();

    private boolean requestHandled = false;


    /**
     * By default the content of the "default" model is used both during
     * rendering and redirect scenarios. Alternatively controller methods
     * can declare an argument of type {@code RedirectAttributes} and use
     * it to provide attributes to prepare the redirect URL.
     * <p>Setting this flag to {@code true} guarantees the "default" model is
     * never used in a redirect scenario even if a RedirectAttributes argument
     * is not declared. Setting it to {@code false} means the "default" model
     * may be used in a redirect if the controller method doesn't declare a
     * RedirectAttributes argument.
     * <p>The default setting is {@code false}.
     */
    public void setIgnoreDefaultModelOnRedirect(boolean ignoreDefaultModelOnRedirect) {
        this.ignoreDefaultModelOnRedirect = ignoreDefaultModelOnRedirect;
    }

    /**
     * Set a view name to be resolved by the DispatcherServlet via a ViewResolver.
     * Will override any pre-existing view name or View.
     */
    public void setViewName(@Nullable String viewName) {
        this.view = viewName;
    }

    /**
     * Return the view name to be resolved by the DispatcherServlet via a
     * ViewResolver, or {@code null} if a View object is set.
     */
    @Nullable
    public String getViewName() {
        return (this.view instanceof String ? (String) this.view : null);
    }

    /**
     * Set a View object to be used by the DispatcherServlet.
     * Will override any pre-existing view name or View.
     */
    public void setView(@Nullable Object view) {
        this.view = view;
    }

    /**
     * Return the View object, or {@code null} if we using a view name
     * to be resolved by the DispatcherServlet via a ViewResolver.
     */
    @Nullable
    public Object getView() {
        return this.view;
    }

    /**
     * Whether the view is a view reference specified via a name to be
     * resolved by the DispatcherServlet via a ViewResolver.
     */
    public boolean isViewReference() {
        return (this.view instanceof String);
    }

    /**
     * Return the model to use -- either the "default" or the "redirect" model.
     * The default model is used if {@code redirectModelScenario=false} or
     * there is no redirect model (i.e. RedirectAttributes was not declared as
     * a method argument) and {@code ignoreDefaultModelOnRedirect=false}.
     */
    public ModelMap getModel() {
        if (useDefaultModel()) {
            return this.defaultModel;
        } else {
            if (this.redirectModel == null) {
                this.redirectModel = new ModelMap();
            }
            return this.redirectModel;
        }
    }

    /**
     * Whether to use the default model or the redirect model.
     */
    private boolean useDefaultModel() {
        return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
    }

    /**
     * Return the "default" model created at instantiation.
     * <p>In general it is recommended to use {@link #getModel()} instead which
     * returns either the "default" model (template rendering) or the "redirect"
     * model (redirect URL preparation). Use of this method may be needed for
     * advanced cases when access to the "default" model is needed regardless,
     * e.g. to save model attributes specified via {@code @SessionAttributes}.
     *
     * @return the default model (never {@code null})
     * @since 4.1.4
     */
    public ModelMap getDefaultModel() {
        return this.defaultModel;
    }

    /**
     * Provide a separate model instance to use in a redirect scenario.
     * The provided additional model however is not used unless
     * {@link #setRedirectModelScenario(boolean)} gets set to {@code true} to signal
     * a redirect scenario.
     */
    public void setRedirectModel(ModelMap redirectModel) {
        this.redirectModel = redirectModel;
    }

    /**
     * Whether the controller has returned a redirect instruction, e.g. a
     * "redirect:" prefixed view name, a RedirectView instance, etc.
     */
    public void setRedirectModelScenario(boolean redirectModelScenario) {
        this.redirectModelScenario = redirectModelScenario;
    }

    /**
     * Provide an HTTP status that will be passed on to with the
     * {@code ModelAndView} used for view rendering purposes.
     *
     * @since 4.3
     */
    public void setStatus(@Nullable HttpStatus status) {
        this.status = status;
    }

    /**
     * Return the configured HTTP status, if any.
     *
     * @since 4.3
     */
    @Nullable
    public HttpStatus getStatus() {
        return this.status;
    }

    /**
     * Programmatically register an attribute for which data binding should not occur,
     * not even for a subsequent {@code @ModelAttribute} declaration.
     *
     * @param attributeName the name of the attribute
     * @since 4.3
     */
    public void setBindingDisabled(String attributeName) {
        this.bindingDisabled.add(attributeName);
    }

    /**
     * Whether binding is disabled for the given model attribute.
     *
     * @since 4.3
     */
    public boolean isBindingDisabled(String name) {
        return (this.bindingDisabled.contains(name) || this.noBinding.contains(name));
    }

    /**
     * Register whether data binding should occur for a corresponding model attribute,
     * corresponding to an {@code @ModelAttribute(binding=true/false)} declaration.
     * <p>Note: While this flag will be taken into account by {@link #isBindingDisabled},
     * a hard {@link #setBindingDisabled} declaration will always override it.
     *
     * @param attributeName the name of the attribute
     * @since 4.3.13
     */
    public void setBinding(String attributeName, boolean enabled) {
        if (!enabled) {
            this.noBinding.add(attributeName);
        } else {
            this.noBinding.remove(attributeName);
        }
    }

    /**
     * Return the {@link SessionStatus} instance to use that can be used to
     * signal that session processing is complete.
     */
    public SessionStatus getSessionStatus() {
        return this.sessionStatus;
    }

    /**
     * Whether the request has been handled fully within the handler, e.g.
     * {@code @ResponseBody} method, and therefore view resolution is not
     * necessary. This flag can also be set when controller methods declare an
     * argument of type {@code ServletResponse} or {@code OutputStream}).
     * <p>The default value is {@code false}.
     */
    public void setRequestHandled(boolean requestHandled) {
        this.requestHandled = requestHandled;
    }

    /**
     * Whether the request has been handled fully within the handler.
     */
    public boolean isRequestHandled() {
        return this.requestHandled;
    }

    /**
     * Add the supplied attribute to the underlying model.
     * A shortcut for {@code getModel().addAttribute(String, Object)}.
     */
    public ModelAndViewContainer addAttribute(String name, @Nullable Object value) {
        getModel().addAttribute(name, value);
        return this;
    }

    /**
     * Add the supplied attribute to the underlying model.
     * A shortcut for {@code getModel().addAttribute(Object)}.
     */
    public ModelAndViewContainer addAttribute(Object value) {
        getModel().addAttribute(value);
        return this;
    }

    /**
     * Copy all attributes to the underlying model.
     * A shortcut for {@code getModel().addAllAttributes(Map)}.
     */
    public ModelAndViewContainer addAllAttributes(@Nullable Map<String, ?> attributes) {
        getModel().addAllAttributes(attributes);
        return this;
    }

    /**
     * Copy attributes in the supplied {@code Map} with existing objects of
     * the same name taking precedence (i.e. not getting replaced).
     * A shortcut for {@code getModel().mergeAttributes(Map<String, ?>)}.
     */
    public ModelAndViewContainer mergeAttributes(@Nullable Map<String, ?> attributes) {
        getModel().mergeAttributes(attributes);
        return this;
    }

    /**
     * Remove the given attributes from the model.
     */
    public ModelAndViewContainer removeAttributes(@Nullable Map<String, ?> attributes) {
        if (attributes != null) {
            for (String key : attributes.keySet()) {
                getModel().remove(key);
            }
        }
        return this;
    }

    /**
     * Whether the underlying model contains the given attribute name.
     * A shortcut for {@code getModel().containsAttribute(String)}.
     */
    public boolean containsAttribute(String name) {
        return getModel().containsAttribute(name);
    }


    /**
     * Return diagnostic information.
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("ModelAndViewContainer: ");
        if (!isRequestHandled()) {
            if (isViewReference()) {
                sb.append("reference to view with name '").append(this.view).append("'");
            } else {
                sb.append("View is [").append(this.view).append(']');
            }
            if (useDefaultModel()) {
                sb.append("; default model ");
            } else {
                sb.append("; redirect model ");
            }
            sb.append(getModel());
        } else {
            sb.append("Request handled directly");
        }
        return sb.toString();
    }

}
